package org.dei.perla.utils.dataconverter;

import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import javax.tools.ToolProvider;

import org.dei.perla.utils.dataconverter.annotations.CompositeField;
import org.dei.perla.utils.dataconverter.annotations.FixedLengthArray;
import org.dei.perla.utils.dataconverter.annotations.SimpleField;
import org.dei.perla.utils.dataconverter.annotations.StructInfo;
import org.dei.perla.utils.dataconverter.enums.Endianness;
import org.dei.perla.utils.dataconverter.enums.PaddingPosition;

public class NewDataConverter {
	
	private static final String SOURCE_CACHE_PATH = "src/org/dei/perla/utils/dataconverter/cache/";
	private static final String CACHE_PACKAGE = "org.dei.perla.utils.dataconverter.cache.";
	
	private static ReadWriteLock lock = new ReentrantReadWriteLock();
	private static Map<String, ObjectMarshaler<?>> hashMap = new HashMap<String, ObjectMarshaler<?>>();
	
	/**
	 * Esegue la conversione da array di Byte ad oggetto. I Byte in eccesso presenti nell'array sono
	 * restituiti dalla funzione
	 * @param converted Oggetto in cui inserire i dati presenti nell'array toConvertByteArray 
	 * @param toConvertByteArray Array di Byte da caricare nell'oggetto converted passato come parametro
	 * @return Array di Byte non utilizzati durante la procedura
	 * @throws IllegalArgumentException nel caso l'oggetto passato come parametro non sia opportunamente annotato
	 * oppure l'array di Byte non contenga abbastanza elementi
	 */
	public static <T> Byte[] fromByteArray(T converted, Byte[] toConvertByteArray) {
		@SuppressWarnings("unchecked")
		ObjectMarshaler<T> objectMarshaler = (ObjectMarshaler<T>) getObjectMarshaler(converted.getClass());
		return objectMarshaler.marshal(converted, toConvertByteArray);
	}
	
	/**
	 * Converte un array di Byte in un Array di oggetti
	 * @param converted
	 * @param field oggetto che descrive il campo da convertire
	 * @param toConvertByteArrayList ArrayList contenente i Bytes da utilizzare per la conversione
	 * @param endianness Formato dei Byte nell'ArrayList
	 * @return Oggetto rappresentante l'array in cui sono stati caricati i dati presenti nell'ArrayList
	 * @throws IllegalArgumentException
	 */
	public static <T> Byte[] toByteArray(T toConvert) {
		@SuppressWarnings("unchecked")
		ObjectMarshaler<T> objectMarshaler = (ObjectMarshaler<T>) getObjectMarshaler(toConvert.getClass());
		return objectMarshaler.unmarshal(toConvert);
	}
	
	/**
	 * Retrieves an ObjectMarshaler for the class passed as a parameter. The
	 * ObjectMarshaler is created if not found. 
	 * @param dataClass data class for which the ObjectMarshaler is requested
	 * @return ObjectMarshaler required to serialize and deserialize the
	 * requested class
	 */
	private static <T> ObjectMarshaler<T> getObjectMarshaler(Class<T> dataClass) {
		ObjectMarshaler<T> objectMarshaler;
		// Trying to get the reference to the ObjectMarshaler. Just read lock
		lock.readLock().lock();
		objectMarshaler = (ObjectMarshaler<T>) hashMap.get(dataClass.getName());
		lock.readLock().unlock();
		// If the ObjectMarshaler hasn't been found, get a read lock and
		// check again
		if (objectMarshaler == null) {
			lock.writeLock().lock();
			objectMarshaler = (ObjectMarshaler<T>) hashMap.get(dataClass.getName());
			// If the ObjectMarshaler is not present, create it and store it
			try {
				if (objectMarshaler == null) {
					objectMarshaler = instantiateObjectMarshaler(dataClass); 
					hashMap.put(dataClass.getName(), objectMarshaler);
				}
				return objectMarshaler;
			} finally {
				lock.writeLock().unlock();
			}
		}
		return objectMarshaler;
	}
	
	/**
	 * Crea una sottoclasse di ObjectMarshaler per la serializzazione e
	 * deserializzazione della classe passata come parametro.
	 * La classe creata e' posta nel package org.dei.perla.utils.dataconverter.cache
	 * @param dataClass classe per cui creare l'oggetto ObjectMarshaler
	 */
	public static <T> ObjectMarshaler<T> instantiateObjectMarshaler(Class<T> dataClass) throws RuntimeException {
		StringBuffer mainCode = new StringBuffer();
		mainCode.append("package org.dei.perla.utils.dataconverter.cache;\n\n");
		
		mainCode.append("import org.dei.perla.utils.dataconverter.ObjectMarshaler;\n\n");
		
		mainCode.append("import java.util.Arrays;\n");
		mainCode.append("import java.util.ArrayList;\n");
		mainCode.append("import java.util.Collections;\n");
		mainCode.append("import java.util.List;\n\n");
		
		mainCode.append("import org.dei.perla.utils.dataconverter.enums.Endianness;\n");
		mainCode.append("import org.dei.perla.utils.dataconverter.enums.Sign;\n\n");
		
		mainCode.append("public class " + initialToUpperCase(dataClass.getSimpleName()) +
				"DataConverter extends ObjectMarshaler<" + dataClass.getName() + "> {\n\n");
		
		// Marshaler
		mainCode.append("\tpublic Byte[] marshal(" + dataClass.getName() + " converted, Byte[] toConvertByteArray) {\n");
		mainCode.append("\t\tArrayList<Byte> toConvertByteArrayList = new ArrayList<Byte>();\n");
		mainCode.append("\t\ttoConvertByteArrayList.addAll(Arrays.asList(toConvertByteArray));\n");
		mainCode.append(createObjectMarshalerCode(dataClass, 0));
		
		// Unmarshaler
		mainCode.append("\tpublic Byte[] unmarshal(" + dataClass.getName() + " toConvert) {\n");
		mainCode.append("\t\tArrayList<Byte> byteArrayList = new ArrayList<Byte>();\n");
		mainCode.append(createObjectUnmarshalerCode(dataClass, 0));
		
		// Closing main class
		mainCode.append("}\n");
		
		// Saving new class on disk
		try {
			PrintStream ps = new PrintStream(new File(SOURCE_CACHE_PATH + dataClass.getSimpleName() + "DataConverter.java"));
			ps.print(mainCode.toString());
			ps.flush();
			ps.close();
		} catch (IOException e) {
			throw new RuntimeException("Impossibile salvare l'ObjectMarshaler nel path " + SOURCE_CACHE_PATH);
		}
		
		// Compiling the newly created class
		int output = javax.tools.ToolProvider.getSystemJavaCompiler().run (null, System.out, System.err,
				"-d", "bin", SOURCE_CACHE_PATH + dataClass.getSimpleName() + "DataConverter.java");
		if (output != 0) {
			throw new RuntimeException("Impossibile compilare l'ObjectMarshaler per la classe " + dataClass.getName());
		}
		
		// Loading the newly created class
		try {
			@SuppressWarnings("unchecked")
			ObjectMarshaler<T> objectMarshaler = (ObjectMarshaler<T>) Class.forName(CACHE_PACKAGE + dataClass.getSimpleName() + "DataConverter").newInstance();
			return objectMarshaler;
		} catch (ClassNotFoundException e) {
			throw new RuntimeException("Impossibile istanziare l'ObjectMarshaler " +
					"per la classe " + dataClass.getName() + ":\n" + e.getMessage());
		} catch (InstantiationException e) {
			throw new RuntimeException("Impossibile istanziare l'ObjectMarshaler " +
					"per la classe " + dataClass.getName() + ":\n" + e.getMessage());
		} catch (IllegalAccessException e) {
			throw new RuntimeException("Impossibile istanziare l'ObjectMarshaler " +
					"per la classe " + dataClass.getName() + ":\n" + e.getMessage());
		} 
	}
	
	/**
	 * Crea il codice necessario per la serializzazione del contenuto
	 * di un oggetto di classe dataClass in uno array di Byte
	 * @param dataClass classe da deserializzare
	 * @param depthSuffix suffisso da apporre al nome dei metodi. Utilizzato 
	 * per disambiguare metodi con stesso nome
	 * @return codice per la serializzazione di dataClass
	 */
	private static StringBuffer createObjectUnmarshalerCode(Class<?> dataClass, int depthSuffix) {
		ArrayList<StringBuffer> methods = new ArrayList<StringBuffer>();
		StringBuffer mainCode = new StringBuffer();
		StringBuffer codeSnippet;
		String upperCaseFieldName;
		String methodName;
		int latestDepthSuffix = depthSuffix + 1;
		
		StructInfo structInfo = NewDataConverter.getStructInfoAnnotation(dataClass);
		
		for (Field field : dataClass.getDeclaredFields()) {
			upperCaseFieldName = initialToUpperCase(field.getName());
			// Current field is a Fixed Length Array
			if (field.isAnnotationPresent(FixedLengthArray.class)) {
				checkFixedLengthArrayAnnotations(dataClass, field);
				methods.add(createFixedLengthArrayUnmarshalerCode(field.getAnnotation(FixedLengthArray.class),
						field.getType().getComponentType(), field.getName(), field, structInfo.endianness(), latestDepthSuffix));
				mainCode.append("\t\tbyteArrayList.addAll(unmarshal" +
						upperCaseFieldName + "_" + latestDepthSuffix + "(toConvert.get" +
						upperCaseFieldName + "()));\n");
				latestDepthSuffix++;
			} else if (field.isAnnotationPresent(SimpleField.class)) {
				SimpleField simpleField = field.getAnnotation(SimpleField.class);
				mainCode.append("\t\tbyteArrayList.addAll(storeNumberToByteArray(toConvert.get" + upperCaseFieldName +
						"(), Endianness." + structInfo.endianness() + ", " + simpleField.size() + "));\n");
			} else if (field.isAnnotationPresent(CompositeField.class)) {
				methodName = "unmarshal" + upperCaseFieldName + "_" + depthSuffix;
				mainCode.append("\t\tbyteArrayList.addAll(" + methodName + 
						"(toConvert.get" + upperCaseFieldName + "()));\n");
				codeSnippet = new StringBuffer();
				codeSnippet.append("\tprivate ArrayList<Byte> " +
						methodName + "(" + field.getType().getName() + " toConvert) {\n");
				codeSnippet.append("\t\t\tArrayList<Byte> byteArrayList = new ArrayList<Byte>();\n");
				codeSnippet.append(createObjectUnmarshalerCode(field.getType(), latestDepthSuffix++));
				methods.add(codeSnippet);
			}
		}
		
		if (depthSuffix == 0) {
			mainCode.append("\t\tByte[] output = new Byte[byteArrayList.size()];\n");
			mainCode.append("\t\treturn byteArrayList.toArray(output);\n");
		} else {
			mainCode.append("\t\treturn byteArrayList;\n");
		}
		
		mainCode.append("\t}\n\n");
		
		for (StringBuffer buffer : methods) {
			mainCode.append(buffer + "\n");
		}
		
		return mainCode;
	}
	
	/**
	 * Crea il codice necessario per la serializzazione di un array in un
	 * array di Byte.
	 * @param fixedLengthArray 
	 * @param dataClass classe del campo da deserializzare
	 * @param name nome del campo da deserializzare
	 * @param field oggetto Field per la descrizione del campo da
	 * deserializzare
	 * @param endianness indica se i dati devono essere deserializzati per
	 * una architettura big o little endian
	 * @param depthSuffix suffisso da apporre al nome dei metodi. Utilizzato 
	 * per disambiguare metodi con stesso nome
	 * @return codice necessario per la serializzazione di un array
	 */
	private static StringBuffer createFixedLengthArrayUnmarshalerCode(FixedLengthArray fixedLengthArray,
			Class<?> dataClass, String name, Field field, Endianness endianness, int depthSuffix) {
		StringBuffer code = new StringBuffer();
		StringBuffer codeSnippet = new StringBuffer();
		String methodName;
		int elementSize = 0;
		String upperCaseFieldName = initialToUpperCase(name);
		
		code.append("\tprivate ArrayList<Byte> unmarshal" + initialToUpperCase(name) +
				"_" + depthSuffix + "(" + dataClass.getName() + "[] toConvert) {\n");
		code.append("\t\tArrayList<Byte> byteArrayList = new ArrayList<Byte>();\n");
		
		// Creating code for converting the array in a Byte ArrayList
		if (field.isAnnotationPresent(SimpleField.class)) {
			SimpleField simpleField = field.getAnnotation(SimpleField.class);
			elementSize = simpleField.size();
			code.append("\t\tfor (int i = 0; i < toConvert.length; i++) {\n");
			code.append("\t\t\tbyteArrayList.addAll(storeNumberToByteArray(toConvert[i], Endianness."
					+ endianness + ", " + simpleField.size() + "));\n");
			code.append("\t\t}\n");
			
		} else if (field.isAnnotationPresent(CompositeField.class)) {
			depthSuffix++;
			CompositeField compositeField = field.getAnnotation(CompositeField.class);
			elementSize = compositeField.size();
			methodName = "unmarshal" + upperCaseFieldName + "ArrayElement_" + depthSuffix;
			code.append("\t\tfor (int i = 0; i < toConvert.length; i++) {\n");
			code.append("\t\t\tbyteArrayList.addAll(" + methodName + 
					"(toConvert[i]));\n");
			code.append("\t\t}\n");
			
			codeSnippet = new StringBuffer();
			codeSnippet.append("\tprivate ArrayList<Byte> " +
					methodName + "(" + dataClass.getName() + " toConvert) {\n");
			codeSnippet.append("\t\tArrayList<Byte> byteArrayList = new ArrayList<Byte>();\n");
			codeSnippet.append(createObjectUnmarshalerCode(dataClass, depthSuffix + 1));
		}
		
		// Creating code for padding addition
		code.append("\t\tint paddingLength = (" + fixedLengthArray.length() + " - toConvert.length) * " + elementSize + ";\n");
		code.append("\t\tList<Byte> padding = new ArrayList<Byte>(paddingLength);\n");
		code.append("\t\tpadding.addAll(Collections.nCopies(paddingLength, (byte)" + fixedLengthArray.padding() + "));\n");
		
		if (fixedLengthArray.paddingPosition() == PaddingPosition.AT_END) {
			code.append("\t\tbyteArrayList.addAll(padding);\n");
		} else {
			code.append("\t\tpadding.addAll(byteArrayList);\n");
			code.append("\t\tbyteArrayList = padding;\n");
		}
		
		code.append("\t\treturn byteArrayList;\n");
		code.append("\t}\n\n");
		code.append(codeSnippet);
		
		return code;
	}
	
	/**
	 * Crea il codice necessario per la deserializzazione del contenuto
	 * di un array di Byte in un oggetto di classe dataClass
	 * @param dataClass classe da deserializzare
	 * @param depthSuffix suffisso da apporre al nome dei metodi. Utilizzato 
	 * per disambiguare metodi con stesso nome
	 * @return codice per la deserializzazione di dataClass
	 */
	private static StringBuffer createObjectMarshalerCode(Class<?> dataClass, int depthSuffix) {
		ArrayList<StringBuffer> methods = new ArrayList<StringBuffer>();
		StringBuffer mainCode = new StringBuffer();
		StringBuffer codeSnippet;
		String upperCaseFieldName;
		String methodName;
		int latestDepthSuffix = depthSuffix + 1;
		
		StructInfo structInfo = NewDataConverter.getStructInfoAnnotation(dataClass);
		
		for (Field field : dataClass.getDeclaredFields()) {
			upperCaseFieldName = initialToUpperCase(field.getName());
			// Current field is a Fixed Length Array
			if (field.isAnnotationPresent(FixedLengthArray.class)) {
				checkFixedLengthArrayAnnotations(dataClass, field);
				codeSnippet = createFixedLengthArrayMarshalerCode(field.getAnnotation(FixedLengthArray.class),
						field.getType().getComponentType(), field.getName(), structInfo.endianness(), field,
						latestDepthSuffix);
				mainCode.append("\t\tconverted.set" + upperCaseFieldName + "(" +
						"marshal" + upperCaseFieldName + "_" + latestDepthSuffix + "(toConvertByteArrayList));\n");
				methods.add(codeSnippet);
				latestDepthSuffix++;
			// Current field is a Simple Field
			} else if (field.isAnnotationPresent(SimpleField.class)) {
				SimpleField simpleField = field.getAnnotation(SimpleField.class);
				mainCode.append("\t\tconverted.set" + upperCaseFieldName + "(" +
						"simpleFieldFromByteArray(" + getObjectTypeName(field.getType()) + ".class, toConvertByteArrayList, "
						+ simpleField.size() + ", Sign." + simpleField.sign() + ", Endianness." + structInfo.endianness() + "));\n");
			// Current field is a Composite Field
			} else if (field.isAnnotationPresent(CompositeField.class)) {
				methodName = "marshal" + upperCaseFieldName + "_" + depthSuffix;
				mainCode.append("\t\tconverted.set" + upperCaseFieldName + "(" +
						methodName + "(toConvertByteArrayList));\n");
				codeSnippet = new StringBuffer();
				codeSnippet.append("\tprivate " + field.getType().getName() + " " +
						methodName + "(ArrayList<Byte> toConvertByteArrayList) {\n");
				codeSnippet.append("\t\t" + field.getType().getName() + " converted = new " + field.getType().getName() + "();\n");
				codeSnippet.append(createObjectMarshalerCode(field.getType(), latestDepthSuffix++));
				methods.add(codeSnippet);
			}
		}
		
		if (depthSuffix == 0) {
			mainCode.append("\t\tByte[] output = new Byte[toConvertByteArrayList.size()];\n");
			mainCode.append("\t\treturn toConvertByteArrayList.toArray(output);\n");
		} else {
			mainCode.append("\t\treturn converted;\n");
		}
		
		mainCode.append("\t}\n\n");
		
		for (StringBuffer buffer : methods) {
			mainCode.append(buffer + "\n");
		}
		
		return mainCode;
	}
	
	/**
	 * Crea il codice necessario per la deserializzazione di un array di Byte in un
	 * array con elementi di classe dataClass
	 * @param fixedLengthArray 
	 * @param dataClass classe del campo da deserializzare
	 * @param name nome del campo da deserializzare
	 * @param field oggetto Field per la descrizione del campo da
	 * deserializzare
	 * @param endianness indica se i dati devono essere deserializzati per
	 * una architettura big o little endian
	 * @param depthSuffix suffisso da apporre al nome dei metodi. Utilizzato 
	 * per disambiguare metodi con stesso nome
	 * @return codice necessario per la deserializzazione di un array
	 */
	private static StringBuffer createFixedLengthArrayMarshalerCode(FixedLengthArray fixedLengthArray,
			Class<?> typeClass, String name, Endianness endianness, Field field, int depthSuffix) {
		StringBuffer code = new StringBuffer();
		StringBuffer codeSnippet = new StringBuffer();
		
		String upperCaseFieldName = initialToUpperCase(name);
		
		code.append("\tprivate " + typeClass.getName() + "[] marshal" + upperCaseFieldName + "_" + depthSuffix +
				"(ArrayList<Byte> toConvertByteArrayList) {\n");
		code.append("\t\t" + typeClass.getName() + "[] converted = new " + typeClass.getName() + "[" + fixedLengthArray.length() + "];\n");
		
		if (field.isAnnotationPresent(SimpleField.class)) {
			SimpleField simpleField = field.getAnnotation(SimpleField.class);
			code.append("\t\tfor (int i = 0; i < " + fixedLengthArray.length() + "; i++) {\n");
			code.append("\t\t\tconverted[i] = " + "simpleFieldFromByteArray(" +
					getObjectTypeName(field.getType().getComponentType()) + ".class, toConvertByteArrayList, "
					+ simpleField.size() + ", Sign." + simpleField.sign() + ", Endianness." + endianness + ");\n");
			code.append("\t\t}\n");
		} else if (field.isAnnotationPresent(CompositeField.class)) {
			depthSuffix++;
			code.append("\t\tfor (int i = 0; i < " + fixedLengthArray.length() + "; i++) {\n");
			code.append("\t\t\tconverted[i] = marshal" + upperCaseFieldName + "ArrayElement_" + depthSuffix + "(toConvertByteArrayList);\n");
			codeSnippet.append("private " + typeClass.getName() + " marshal" + upperCaseFieldName + "ArrayElement_"
					+ depthSuffix + "(ArrayList<Byte> toConvertByteArrayList) {\n");
			codeSnippet.append(typeClass.getName() + " converted = new " + typeClass.getName() + "();\n");
			codeSnippet.append(createObjectMarshalerCode(typeClass, depthSuffix + 1));
			code.append("\t\t}\n");
		}
		
		code.append("\t\treturn converted;\n");
		code.append("\t}\n\n");
		
		code.append(codeSnippet);
		
		return code;
	}
	
	/**
	 * Restituisce il nome dell'oggetto. Nel caso di tipi primitivi
	 * viene restituito il nome dell'oggetto corrispondente.
	 * @param typeClass classe di cui si vuole conoscere il nome
	 * @return Nome della classe passata come parametro.
	 */
	private static String getObjectTypeName(Class<?> typeClass) {
		if (typeClass == byte.class) {
			return "Byte";
		} else if (typeClass == short.class) {
			return "Short";
		} else if (typeClass == int.class) {
			return "Integer";
		} else if (typeClass == long.class) {
			return "Long";
		} else {
			return typeClass.getName();
		}
	}
	
	/**
	 * Mette in maiuscolo il primo carattere della stringa passata come paramentro
	 * @param string stringa da processare
	 * @return La stringa passata come parametro con il primo carattere in maiuscolo
	 */
	private static String initialToUpperCase(String string) {
		return Character.toUpperCase(string.charAt(0)) + string.substring(1);
	}
	
	/**
	 * Restituisce, se presente, l'annotazione StructInfo associata alla classe passata come parametro
	 * @param typeClass Classe da esaminare
	 * @return Annotazione StructInfo relativa alla classe passata come parametro
	 * @throws IllegalArgumentException nel caso la classe non contenga l'annotazione richiesta
	 */
	private static <T> StructInfo getStructInfoAnnotation(Class<T> typeClass) throws IllegalArgumentException {
		if (!typeClass.isAnnotationPresent(StructInfo.class)) {
			throw new IllegalArgumentException("Il tipo " + typeClass.getName()
					+ " passato come parametro non contiene l'annotazione " +
							"StructInfo necessaria per la conversione!");
		}
		return typeClass.getAnnotation(StructInfo.class);
	}
	
	/**
	 * Controlla che il campo dichiarato come FixedLengthArray abbia le annotazioni necessarie per la conversione
	 * e che le dimensioni massime siano rispettate
	 * @param parentObjectClass Classe dell'oggetto che contiene il campo
	 * @param field Campo da controllare
	 * @throws IllegalArgumentException Eccezione lanciata nel caso il campo non abbia le annotazioni richieste
	 */
	private static void checkFixedLengthArrayAnnotations(Class<?> parentObjectClass, Field field) throws IllegalArgumentException {
		if (field.isAnnotationPresent(SimpleField.class) && field.isAnnotationPresent(CompositeField.class)) {
			throw new IllegalArgumentException("L'oggetto di tipo " + parentObjectClass.getName() + 
					" passato come parametro contiene annotazioni errate" +
					" per il metodo di tipo " + field.getName() + " (eliminare @SimpleField o @CompositeField)!");
			
		} else if (!field.isAnnotationPresent(SimpleField.class) && !field.isAnnotationPresent(CompositeField.class)) {
			throw new IllegalArgumentException("L'oggetto di tipo " + parentObjectClass.getClass().getName() +
					" passato come parametro contiene annotazioni errate" +
					" per il metodo di tipo " + field.getName() + " (aggiungere @SimpleField o @CompositeField)!");
			
		} else if (field.isAnnotationPresent(CompositeField.class)) {
			CompositeField compositeField = field.getAnnotation(CompositeField.class);
			if (compositeField.size() == -1) {
				throw new IllegalArgumentException("Il campo " + field.getName() + " dell'oggetto" + parentObjectClass.getName() +
						" e' dichiarato come FixedLengthArray ma l'annotazione degli elementi che lo costituiscono " +
						"manca dell'elemento size!");
			}
		}
	}
}
